define(["dojo/query", "dojo/dom-construct", "dojo/dom-class", "dojo/dom",
        "dojo/window", "dojo/dom-geometry", "dojo/dom-attr", "dojo/on",
        "dojo/hash", "dojo/_base/connect", "dojo/dom-style",
        "dojo/NodeList-traverse", "dojo/NodeList-dom"], 
       function(query, domConstruct, domClass, dom, 
                dwindow, domGeom, domAttr, on, hash, connect, domStyle) {
  // The functions we will make available to the user.
  var retFunctions = {};

  // Current options for our subroutines.
  var optionsObject = {
    'sectionOrder': [ 'chapter', 'section', 'subsection', 'subsubsection' ],
    'showLevel': '',
    'scrollEvents': true,
    'footnoteLevel': 'section'
  };

  var levelSections = [];
         
  var sectionPositions = [];

  var sectionLinks = {
    current: -1,
    previous: -1,
    next: -1
  };

  var hashSetter = function(sectionNumber) {
    if (typeof sectionPositions[sectionNumber] !== 'undefined' &&
        typeof sectionPositions[sectionNumber].id) {
      hash('s=' + sectionPositions[sectionNumber].id);
    }
  };
         
  var scrollHandler = function(evtObj) {
    var vs = dwindow.getBox();
    var i, j;
    j = 0;
    for (i = 1; i < sectionPositions.length; i++) {
      if (vs.t > sectionPositions[i].y) {
        j = i;
      }
    }
    
    if (typeof sectionPositions[j] !== 'undefined' &&
        typeof sectionPositions[j].id !== 'undefined') {
      hashSetter(j);
      sectionLinks.current = j;
    }
    if (typeof sectionPositions[j - 1] !== 'undefined' &&
        typeof sectionPositions[j - 1].name !== 'undefined') {
      sectionLinks.previous = j - 1;
    } else {
      sectionLinks.previous = -1;
    }
    if (typeof sectionPositions[j + 1] !== 'undefined' &&
        typeof sectionPositions[j + 1].name !== 'undefined') {
      sectionLinks.next = j + 1;
    } else {
      sectionLinks.next = -1;
    }
    
    connect.publish('/webdoc/scrolled', [ sectionLinks ]);
  };

  var idGenerator = function(title) {
    // Takes a title and turns it into a unique ID.
    var titleId = title;
    // Remove the spaces.
    var origTitle = titleId.replace(/\s/g, '');
    titleId = origTitle;
    
    // Check if this ID already exists.
    var i = 0;
    while (dom.byId(titleId) != null && i < 10) {
      i++;
      titleId = origTitle + '' + i;
    }
    
    return titleId;
  };

  var titleGenerator = function(titleHtml) {
    var t = titleHtml.replace(/\<\/*span.*?\>/g, ' ');
    
    return t;
  };
         
  var numberSections = function(className, upperClassName, prefix) {
    var n = 1;
    var oldNumber = -1;
    query('.' + className).forEach(function(node, index, nodeList) {
      // Check we don't have an unnumbered section.
      if (domClass.contains(node, 'unnumbered')) {
        return;
      }
      // Get the first child of the child header.
      query('> header >', node).forEach(function(cNode, cIndex, cNodeList) {
        if (cIndex > 0) {
          return;
        }
        if (!domAttr.has(node, 'id')) {
          node.id = idGenerator(cNode.innerHTML);
        }
        var sNumber = domConstruct.create('span', {
          'class': 'sectionNumber' 
        });
        if (typeof upperClassName !== 'undefined' &&
            upperClassName !== null) {
          // Get the children of the siblings of this node.
          query('> header >', node.parentNode).children('.sectionNumber').forEach(function(dNode, dIndex, dNodeList) {
            var newNumber = dNode.innerHTML;
            if (newNumber !== oldNumber) {
              n = 1;
            }
            oldNumber = newNumber;
            sNumber.innerHTML = newNumber + '.';
          });
        }
        sNumber.innerHTML += '' + n;
        domConstruct.place(sNumber, cNode, 'first');
        n++;
        if (!domAttr.has(node, 'name')) {
          node.name = titleGenerator(cNode.innerHTML);
        }
      });
    });
  };

  retFunctions.determinePositions = function(options) {
    options = options || optionsObject;
    // Reset the section positions.
    sectionPositions = [];
    
    var i;
    for (i = 0; i < options.sectionOrder.length; i++) {
      query('.' + options.sectionOrder[i]).forEach(function(node, index, nodeList) {
        var nodePos = domGeom.position(node, true);
        nodePos.id = node.id;
        nodePos.name = node.name;
        nodePos.level = i;
        sectionPositions.push(nodePos);
      });
    }
    
    // Sort the positions now for easy use later.
    sectionPositions.sort(function(a, b) {
                            return (a.y - b.y);
                          });
    scrollHandler();
  };
         
  retFunctions.numberSections = function(options) {
    // Set defaults for the options if required.
    options = options || optionsObject;
    options.sectionOrder = options.sectionOrder || optionsObject.sectionOrder;
    
    var i;
    for (i = 0; i < options.sectionOrder.length; i++) {
      var u = (i > 0) ? options.sectionOrder[i - 1] : null;
      numberSections(options.sectionOrder[i], u, null);
    }
  };

  retFunctions.numberEquations = function(options) {
    options = options || optionsObject;
    // Find the equations.
    var nE = 1;
    query('div.equation').forEach(function(node, index, nodeList) {
      // Make a box for the number.
      var equationLabel = domConstruct.create('div', {
        'class': 'equationLabel',
        innerHTML: ''
      });
      var equationNumber = domConstruct.create('span', {
        'class': 'equationNumber',
        innerHTML: '(' + nE + ')'
      });
      equationLabel.appendChild(equationNumber);
      domConstruct.place(equationLabel, node, 'first');
      nE++;
    });
  };
         
  retFunctions.numberFigures = function(options) {
    options = options || optionsObject;
    // Find the figcaptions.
    var nF = 1;
    var nT = 1;
    query('figcaption').forEach(function(node, index, nodeList) {
      var fType = '';
      var fLabel = '';
      if (domClass.contains(node.parentNode,'image')) {
        fType = 'Figure ';
        fLabel = '' + nF;
        nF++;
      } else if (domClass.contains(node.parentNode,'table')) {
        fType = 'Table ';
        fLabel = '' + nT;
        nT++;
      }
      var figLabel = domConstruct.create('span', {
        'class': 'figureLabel',
        'innerHTML': fType
      });
      var figNumber = domConstruct.create('span', {
        'class': 'figureNumber',
        'innerHTML': fLabel
      });
      figLabel.appendChild(figNumber);
      domConstruct.place(figLabel, node, 'first');
    });
  };

  retFunctions.resolveReferences = function(options) {
    options = options || optionsObject;
    // Find all the referencing nodes.
    query('.' + 'reference').forEach(function(node, index, nodeList) {
      // The innerHTML of this node should be the ID of the figure it
      // references.
      domClass.add(node, 'referenceLinked');
      var refId = node.innerHTML;
      var refNode = dom.byId(refId);
      if (refNode) {
        // What type of thing is this?
        if (refNode.nodeName.toLowerCase() == 'figure') {
          // Get the number of this figure.
          var figNumber = query('.figureNumber', refNode)[0].innerHTML;
          // Replace the node contents with it.
          node.innerHTML = figNumber;
          node.title = refId;
        } else if (refNode.nodeName.toLowerCase() == 'section') {
          // Get the number of this section.
          var sectionNumber = query('.sectionNumber', refNode)[0].innerHTML;
          node.innerHTML = sectionNumber;
          node.title = refId;
        } else if (refNode.nodeName.toLowerCase() == 'div' &&
                   domClass.contains(refNode, 'equation') == true) {
          var equationNumber = query('.equationNumber', refNode)[0].innerHTML.replace(/[\(\)]/g, '');
          node.innerHTML = equationNumber;
          node.title = refId;
        }
      } else {
        console.log('The figure ' + refId + ' is not present on the page.');
      }
    });
  };

  retFunctions.formatLinks = function(options) {
    options = options || optionsObject;
    // Find all the anchors that may need formatting.
    query('a.link').forEach(function(node, index, nodeList) {
      // Check for an empty link.
      var a = node.innerHTML;
      // console.log(node);
      if (a == '') {
        node.innerHTML = node.href;
      }
    });
  };

  retFunctions.scrollToReference = function(evtObj) {
    dwindow.scrollIntoView(evtObj.target.title);
  };
         
  retFunctions.linkReferences = function(options) {
    options = options || optionsObject;
    // Find all the reference spans.
    query('span.reference').
      addClass('referenceLinked').
      on('click', retFunctions.scrollToReference);
  };

  retFunctions.start = function(options) {
    options = options || optionsObject;
    retFunctions.numberSections();
    retFunctions.numberFigures();
    retFunctions.numberEquations();
    retFunctions.resolveReferences();
    retFunctions.formatLinks();
    retFunctions.linkReferences();
    retFunctions.formatFootnotes();
    if (options.scrollEvents) {
      on(window, 'scroll', scrollHandler);
    }
  };

  retFunctions.sectionInfo = function(sectionNumber) {
    return sectionPositions[sectionNumber];
  };

  var foundIt = function(nodeRef, nodeType) {
    var findTag = true;
    var nodeCheck = nodeType.toLowerCase();
    if (/^\./.test(nodeType)) {
      findTag = false;
      nodeCheck = nodeType.replace(/^\./, '');
    }
    if (findTag === true &&
        nodeRef.nodeName.toLowerCase() === nodeCheck) {
      return true;
    } else if (findTag === false &&
               domClass.contains(nodeRef, nodeCheck)) {
      return true;
    }
    return false;
  };
         
  var moveUp = function(nodeRef, nodeType) {
    // Move up the document tree from nodeRef until we find a node with tag nodeType.
    var rNode = nodeRef;
    while (!foundIt(rNode, nodeType)) {
      rNode = rNode.parentNode;
    }
    return rNode;
  };

  retFunctions.hiderStart = function(options) {
    options = options || optionsObject;
    // Find all the appropriate sections at this level.
    if (options.showLevel === '') {
      // No hiding is required.
      return;
    }
    // Find the level.
    var l = -1;
    for (var i = 0; i < options.sectionOrder.length; i++) {
      if (options.sectionOrder[i] === options.showLevel) {
        l = i;
        break;
      }
    }
    if (l < 0) {
      // The level wasn't found.
      return;
    }
    
    // Make sure we have determined the order already.
    if (sectionPositions.length < 1) {
      retFunctions.determinePositions(options);
    }
    // Now find all the hideable elements.
    levelSections = [];
    for (var i = 0; i < sectionPositions.length; i++) {
      if (sectionPositions[i].level <= l) {
        levelSections.push({ 
          id: sectionPositions[i].id,
          'hidden': false
        });
      }
    }
  };
         
  retFunctions.formatFootnotes = function(options) {
    options = options || optionsObject;
    var cFootnote = 0;
    // Find all the footnotes.
    query('span.footnote').forEach(function (node, index, nodeList) {
      cFootnote++;
      var footText = node.innerHTML;
      node.innerHTML = cFootnote;
      // Where do we display this footnote?
      var footnoteArea;
      if (node.parentNode.nodeName.toLowerCase() === 'td' ||
          node.parentNode.nodeName.toLowerCase() === 'th') {
        // We're in a table, so we display the footnote just above the caption.
        // Find the top level table node.
        var tableNode = moveUp(node, 'table');
        // Now find the caption or make it if it's not there.
        footnoteArea = query('> caption', tableNode)[0];
        if (!footnoteArea) {
          // Make the caption, and it has to go at the top of the table.
          footnoteArea = domConstruct.create('caption', {
            'class': 'tableFootnotes'
          });
          domConstruct.place(footnoteArea, tableNode, 'first');
        }
      } else {
        // We're outside a table, so we display the footnote at the bottom
        // of the relevant section.
        var sectionNode = moveUp(node, '.' + options.footnoteLevel);
        // Now find the footnote area, or make it if it's not there.
        footnoteArea = query('> span.footnotes', sectionNode)[0];
        if (!footnoteArea) {
          // Make the footnote area, and it has to go at the bottom of
          // the section.
          footnoteArea = domConstruct.create('span', {
            'class': 'footnotes'
          });
          domConstruct.place(footnoteArea, sectionNode, 'last');
        }
      }
      var footNote = domConstruct.create('div', {
        'class': 'footnoteText'
      });
      var footNumber = domConstruct.create('span', {
        'class': 'footnote',
        innerHTML: cFootnote
      });
      footNote.appendChild(footNumber);
      var footNoteText = domConstruct.create('span', {
        innerHTML: footText
      });
      footNote.appendChild(footNoteText);
      footnoteArea.appendChild(footNote);
    });
  };
         
  return retFunctions;
});
